Ana içeriğe geç
  1. 100 Günde SwiftUI Notları/

53.Gün - SwiftUI Binding, TextEditor ve SwiftData Giriş

Bugün yeni bir projeye başlıyoruz ve burada işler gerçekten ciddileşmeye başlıyor çünkü, projeyi oluştururken ve işinize yarayacak önemli bir yeni Swift becerisi öğreneceğiz.

Öğreneceğimiz beceri, SwiftData’dır. Okuma, yazma, filtreleme, sıralama ve daha fazlası dahil olmak üzere bir veritabanındaki nesneleri yönetmekten sorumludur ve iOS, macOS ve ötesi için uygulama geliştirmede son derece önemlidir. Daha önce verilerimizi doğrudan UserDefaults’a yazdık, ancak bu sadece öğrenmenize yardımcı olacak kısa vadeli bir şeydi, SwiftData ise daha kapsamlı.

Giriş #

Bu projede hangi kitapları okuduğunuzu ve onlar hakkında ne düşündüğünüzü takip etmek için bir uygulama oluşturacağız.

Bu kez Apple’ın veritabanları ile çalışan framework’ü SwiftData ile tanışacaksınız. Bu proje SwiftData için bir giriş niteliğinde olacak, ancak yakında çok daha fazla ayrıntıya gireceğiz.

Aynı zamanda, ilk custom user component’i de oluşturacağız, kullanıcının her kitap için bir puan bırakmak üzere dokunabileceği bir yıldız derecelendirme widget’ı. Bu, sizi @Binding adı verilen başka bir property wrapper ile tanıştırmak anlamına gelecek.

Bu çalılma için, Bookworm adında bir xcode projesi başlatın. (Projeyi oluştururken SwiftData seçeneğini seçmeyin.)

@Binding ile Custom Component Oluşturma #

SwiftUI’nin @State property wrapper’ının local value türler ile çalışmamıza nasıl izin verdiğini ve @Bindable’ın observable sınıflar içindeki özelliklere nasıl binding yapmamıza izin verdiğini zaten gördünüz. Oldukça kafa karıştırıcı bir isme sahip üçüncü bir seçenek daha var: @Binding , bu bir view’ın basit bir @State property’sini diğerleriyle paylaşmamızı sağlar, böylece her ikisi de aynı Int, String, Boolean vb. işret eder.

Bir toggle switch oluşturduğumuzda, bunun gibi bir Boolean property göndeririz

@State private var rememberMe = false

var body: some View {
    Toggle("Remember Me", isOn: $rememberMe)
}

Kullanıcı switch ile etkileşime girdiğinde geçişin Boolean’ı değiştirmesi gerekiyor, ancak hangi değeri değiştirmesi gerektiğini nasıl hatırlıyor?

İşte @Binding burada devreye giriyor: aslında başka bir yerden başka bir değere işaret eden bir view’da tek bir değiştirilebilir değer saklamamıza izin veriyor. Toggle örneğinde, switch kendi local binding’ini bir Boolean olarak değiştirir, ancak perde arkasında bu aslında view’daki @State property’yi manipüle eder- her ikisi de aynı Boolean’ı okur ve yazar.

@Bindable ve @Binding arasındaki fark ilk başta çok kafa karıştırıcı olacaktır, ancak eninde sonunda anlaşılacaktır.

Açık olmak gerekirse @Bindable , @Observable makrosunu kullanan paylaşılan bir sınıfa erişirken kullanılır. Bir view’da @State kullanarak oluşturursunuz, böylece orada binding’e sahip olursunuz, ancak diğer view’larla paylaşırken @Bindable kullanırsınız, böylece SwiftUI orada da binding oluşturabilir.

Öte yandan @Binding , ayrı bir sınıf yerine basit, value type bir veri parçasına sahip olduğunuzda kullanılır. Örneğin, bir Boolean, Int vb. saklayan bir @State property’imiz var ve bunu aktarmak istiyorsunuz. Bu @Observable makrosunu kullanmaz, bu nedenle @Bindable kullanamayız. Bunun yerine, @Binding kullanırız, böylece Boolean veya Int’i birkaç yerde paylaşabiliriz.

Bu davranış, custom bir kullanıcı arayüzü bileşeni oluşturmak istediğinizde @Binding’i son derece önemli hale getirir. Özünde, UI bileşenleri diğer her şey gibi sadece SwiftUI view’larıdır, ancak @Binding onları ayıran şeydir: yerel @State property’lerine sahip olsalar da, diğer view’lar ile doğrudan arayüz oluşturmalarını sağlayan @Binding property’lerini de açığa çıkarırlar.

Bunu göstermek için, basıldığında aşağıda kalan custom bir buton oluşturmak için gereken koda bakacağız. Temel uygulamamız daha önce gördüğünüz şeyler olacak: biraz dolgulu bir buton, arka plan için doğrusal bir gradyan, bir Capsule clip shape vb. bunun şimdi ContentView.swift’e ekleyin;

struct PushButton: View {
    let title: String
    @State var isOn: Bool

    var onColors = [Color.red, Color.yellow]
    var offColors = [Color(white: 0.6), Color(white: 0.4)]

    var body: some View {
        Button(title) {
            isOn.toggle()
        }
        .padding()
        .background(LinearGradient(colors: isOn ? onColors : offColors, startPoint: .top, endPoint: .bottom))
        .foregroundStyle(.white)
        .clipShape(.capsule)
        .shadow(radius: isOn ? 0 : 5)
    }
}

Buradaki tek heyecan verici şey, iki gradient renk için property kullanmamdır, böylece butonu oluşturan şey tarafından özelleştirilebilirler.

Şimdi bu butonlardan birini ana kullanıcı arayüzümüzün bir parçası olarak aşağıdaki gibi oluşturabiliriz;

struct ContentView: View {
    @State private var rememberMe = false

    var body: some View {
        VStack {
            PushButton(title: "Remember Me", isOn: rememberMe)
            Text(rememberMe ? "On" : "Off")
        }
    }
}

Bu butonun altında bir text view vardır, böylece butonun durumunu izleyebiliriz - kodunuzu çalıştırmayı deneyin ve nasıl çalıştığını görün.

Binding custom view not change state

Göreceğiniz şey, butona dokunmanın gerçekten de butonun görünümünü etkilediğidir, ancak text field bu değişikliği yansıtmaz, her zaman “Off” yazar. Açıkça bir şeyler değişiyor çünkü butona basıldığında butonun görünümü değişiyor, ancak bu değişiklik ContentView’a yansıtılmıyor.

Burada olan şey, tek yönlü bir veri akışı tanımlamış olmamızdır: ContentView, bir PushButton oluşturmak için kullanılan rememberMe Boolean’a sahiptir - butonun ContentView tarafından sağlanan bir başlangıç değeri vardır. Ancak, buton oluşturulduktan sonra değerin kontrolünü devralır, yani isOn property butonun içinde true veya false arasında değişir, ancak bu değişikliği ContentView’a geri iletmez.

Bu bir sorun, çünkü artık iki doğruluk kaynağımız var: ContentView bir değeri, PushButton ise başka bir değeri depoluyor. Neyse ki, @Binding burada devreye giriyor ve PushButton ile onu kullanan şey arasında iki yollu bir bağlantı oluşturmamızı sağlıyor, böylece bir değer değiştiğinde diğeri de değişiyor.

@Binding ’e geçmek için sadece iki değişiklik yapmamız gerekiyor. İlk olarak, PushButton’da isOn property’yi şu şekilde değiştirin;

@Binding var isOn: Bool

İkinci olarak, ContentView’da butonu oluşturma şeklimizi şu şekilde değiştirin;

PushButton(title: "Remember Me", isOn: $rememberMe)

Bu, rememberMe’den önce bir dolar işareti ekler, böylelikle binding’in kendisini iletiriz, içindeki Boolean’ı değil.

Şimdi kodu tekrar çalıştırın ve her şeyin beklendiği gibi çalıştığını göreceksiniz.

Binding custom view change state

Bu @Binding ’in gücüdür: buton söz konusu olduğunda sadece bir Boolean’ı değiştirir başka bir şeyin bu Boolean’ı izlediğinden ve değişikliklere göre hareket ettiğinden haberi yoktur.

TextEditor ile Çok Satırlı Metin Girişini Kabul Etme #

SwiftUI’nin TextField view’ını daha önce birkaç kez kullandık ve kullanıcının kısa metin parçaları girmek istediği zamanlar için harikadır. Bununla birlikte, daha uzun metin parçaları için bunun yerine TextEditor view’ı kullanmaya geçmek isteyebilirsiniz: bu view da bir text string’e two-way binding verilmesini bekler, ancak birden fazla metin satırına izin verme gibi ek bir avantaja sahiptir.

Çoğunlukla yapılandırma seçeneklerinde özel bir şey olmadığı için, TextEditor kullanmak aslında TextField kullanmaktan daha kolaydır, stilini ayarlayamaz veya placeholder ekleyemezsiniz sadece bir string’e bind edersiniz. Bununla birlikte, safe area’nın dışına çıkmadığından emin olmak için dikkatli olmanız gerekir, aksi takdirde yazmak zor olacaktır; NavigationStack, Form vb. içinde kullanmak daha mantıklıdır.

Örneğin, TextEditor ile @AppStorage’ı aşağıdaki gibi birleştirerek dünyanın en basit not uygulamasını oluşturabiliriz.

İpucu : @AppStorage önemli bilgileri depolamak için tasarlanmamıştır, bu nedenle asla özel (private) bir şey için kullanmayın.

Fakat SwiftUI’nin bazı durumlarda daha iyi çalışan üçüncü bir seçeneği var.

Bir TextField oluşturduğumuzda, isteğe bağlı olarak büyüyebileceği bir eksen sağlayabiliriz. Bu, textfield’ın normal tek satırlı bir metin alanı olarak başladığı, ancak kullanıcı yazdıkça tıpkı iMessage text box gibi büyüyebileceği anlamına gelir.

İşte böyle görünüyor;

struct ContentView: View {
    @AppStorage("notes") private var notes = ""

    var body: some View {
        NavigationStack {
            TextField("Enter your text", text: $notes, axis: .vertical)
                .textFieldStyle(.roundedBorder)
                .navigationTitle("Notes")
                .padding()
        }
    }
}

Bu yaklaşımların her ikisini de bir noktada, ancak farklı zamanlarda kullanacaksınız. TextField’ın otomatik olarak genişlemesini sevsem de, bazen kullanıcınıza büyük bir text field gösterebilmek yararlı olabilir, böylece oraya çok şey yazabileceklerini önceden bilirler.

İpucu : SwiftUI genellikle bir Form’un içine girdikten sonra bazı şeylerin görünümünü değiştirir, bu nedenle nasıl değiştiklerini görmek için bunları hem Form’un içinde hem de dışında denediğinizden emin olun.

SwifData ‘ya Giriş #

SwiftUI, Apple’ın tüm platformlarında harika uygulamalar geliştirmeye yönelik güçlü ve modern bir framework. SwiftData ise verileri depolamaya, sorgulamaya ve filtrelemeye yönelik güçlü ve modern bir framework. Bir şekilde birbirlerine uysalar güzel olmaz mıydı?

Sadece birlikte mükemmel bir şekilde çalışmakla kalmıyor, aynı zamanda o kadar az kod gerektiriyorlar ki sonuçlara inanamayacaksınız - sadece birkaç dakika içinde olağanüstü şeyler yaratabilirsiniz.

İlk olarak, temel bilgiler: SwiftData bir object graph ve persistance framework’üdür. Bu da nesneleri ve bu nesnelerin özelliklerini tanımlamamıza ve ardından bunları kalıcı depolamada okuyup yazmamıza izin verdiğini söylemenin süslü bir yoludur.

Görünüşte bu Codable ve UserDefaults kullanmaya benziyor, ancak bundan çok daha gelişmiş: SwiftData verilerimizi sıralama ve filtreleme yeteneğine sahiptir ve çok daha büyük verilerle çalışabilir - ne kadar veri depolayacağının fiilen bir sınırı yoktur. Daha da iyisi, SwiftData ihtiyaç duyduğunuz zamanlar için her türlü daha gelişmiş işlevleri uygulayabilir : iCloud senkronizasyonu, verilerin lazy yüklenmesi, undo ve redo vb.

Bu projede SwiftData’nın gücünün sadece küçük bir kısmını kullanacağız, ancak bu yakında genişleyecek - sadece ilk başta size bir tat vermek istiyorum.

Xcode projenizi oluşturduğunuzda SwiftData desteğini etkinleştirmemenizi istemiştim, çünkü sıkıcı kurulum kodlarının bir kısmını ortadan kaldırsa da anlamsız ve silinmesi gereken bir sürü ekstra örnek kod da ekliyor.

Bunun yerine SwiftData’yı elle nasıl kuracağınızı öğreneceksiniz. Uygulamamızda kullanmak istediğimiz verileri tanımlamamızla başlayan üç adımdan oluşur.

Student.swift isimli dosyayı oluşturun ve aşağıdaki kodu yazın lütfen.

@Observable
class Student {
    var id: UUID
    var name: String

    init(id: UUID, name: String) {
        self.id = id
        self.name = name
    }
}

Çok küçük iki değişiklik yaparak bunu SwiftData nesnesine (veritabanına kaydedebileceği, iCloud ile senkronizasyon edebileceği, arayabileceği, sıralayabileceği ve daha fazlasını yapabileceği bir şey) dönüştürebiliriz.

Öncelikle dosyanın en üstüne başka bir import eklememiz gerekiyor.

import SwiftData

Bu, Swift’e SwiftData’daki tüm işlevselliği getirmek istediğimizi söyler. Ve şimdi bunu değiştirmek istiyoruz.

@Observable
class Student {

Buna

@Model
class Student {

…ve hepsi bu kadar. SwiftData’ya student’leri yüklemek ve kaydetmek için ihtiyaç duyduğu tüm bilgileri vermek için gereken tek şey bu. Ayrıca artık onları sorgulayabilir, silebilir, diğer nesnelerle ilişkilendirebilir ve daha fazlasını yapabilir.

Bu sınıfa SwiftData modeli denir: uygulamalarımızda çalışmak istediğimiz bir tür veriyi tanımlar. Sahne arkasında @Model , @Observable ‘ın kullanıldığı observation sisteminin üzerine inşa edilir, bu da SwiftUI ile gerçekten iyi çalıştığı anlamına gelir.

Artık çalışmak istediğimiz verileri tanımladığımıza göre, SwiftData’yı kurmanın ikinci adımına geçebiliriz: bu modeli yüklemek için küçük bir Swift kodu yazmak. Bu kod SwiftData’ya iPhone’da bizim için Student nesnelerini okuyup yazacağı bir depolama alanı hazırlamasını söyleyecek.

Bu iş en iyi App struct’ta yapılır. Şimdiye kadar yaptığımız tüm projeler de dahil olmak üzere her projede App struct vardır ve çalıştırdığımız tüm uygulama için launch pad görevi görür.

Bu projenin adı Bookworm olduğu için, App struct BookwormApp.swift dosyasının içinde olacaktır. Bu şekilde görünmelidir;

import SwiftUI

@main
struct BookwormApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Normal view kodumuza biraz benzediğini görebilirsiniz: hala bir import SwiftUI ‘imiz var, özel bir tür oluşturmak için hala bir struct kullanıyoruz ve ContentView’ımız tam orada. Geri kalanı yeni ve gerçekten iki bölümle ilgileniyoruz:

  1. @main satırı Swift’e uygulamamızı başlayan şeyin bu olduğunu söyler. Kullanıcı uygulamamızı iOS Ana Ekranından (Home Screen) başlattığında, dahili olarak tüm programı başlatan şey budur.
  2. WindowGroup kısmı SwiftUI’ye uygulamamızın birçok pencerede görüntülenebileceğini söyler. Bu iPhone’da pek işe yaramaz, ancak iPad ve macOs’ta çok daha önemli hale gelir.

Burada SwiftData’ya kullanmamız için tüm depolama alanını ayarlamasını söylememiz gerekiyor ve bu da yine çok küçük iki değişiklik gerektiriyor.

Öncelikle import SwiftUI’nin yanına import SwiftData’yı eklememiz gerekiyor.

İkinci olarak, SwiftData’nın uygulamamızın her yerinde kullanılabilir olması için WindowGroup’a bir modifier eklememiz gerekir.

.modelContainer(for: Student.self) 

Model container, SwiftData’nın verilerini depoladığı yere verdiği addır. Uygulamanız ilk kez çalıştığında bu, SwiftData’nın temel veritabanı dosyasını oluşturması gerektiği anlamına gelir, ancak sonraki çalıştırmalarda daha önce oluşturduğu veritabanını yükleyecektir.

Bu noktada @Model kullanarak veri modellerinin nasıl oluşturulacağını ve modelContainer() modifier’ı kullanılarak bir model cotainer’ının nasıl oluşturulacağını gördünüz. Yapbozun üçüncü parçası model context olarak adlandırılır ve bu da verilerinizin “canlı” versiyonudur - nesneleri yüklediğinizde ve onları değiştirdiğinizde, bu değişiklikler kaydedilene kadar yalnızca bellekte bulunur. Dolayısıyla, model context’in görevi, tüm verilerimizle bellekte bulunur. Dolayısıyla, model context’in görevi, tüm verilerimizle bellekte çalışmamıza izin vermektir; bu verileri sürekli olarak diske okuyup yazmaktan çok daha hızlıdır.

Her SwiftData uygulaması çalışmak için bir model context’e ihtiyaç duyar ve biz zaten bizimkini oluşturduk, modelContainer() modifier’ını kullandığımızda otomatik olarak oluşturulur. SwiftData bizim için otomatik olarak main context adı verilen bir model context oluşturur ve bunu SwiftUI environment’te saklar,

Tüm SwiftData yapılandırmamız tamamlandı, şimdi sıra eğlenceli kısımda : veri okuma ve yazma.

SwiftData’dan bilgi almak bir sorgu kullanarak yapılır - ne istediğimizi, nasıl sıralanması gerektiğini ve herhangi bir filtrenin kullanılıp kullanılmayacağını açıklarız ve SwiftData eşleşen tüm verileri geri gönderir. Bu sorgunun zaman içinde güncel kaldığından emin olmamız gerekir, böylece student oluşturuldukça ve kaldırıldıkça kullanıcı arayüzümüz senkronize kalır.

SwiftUI’nin bunun için bir çözümü var ve -tahmin ettiğiniz gibi- bu başka bir property wrapper. bu kez adı @Query ve bir dosyaya import SwiftData eklediğiniz anda kullanılabilir.

Bu nedenle, ContentView.swift dosyasının en üstüne SwiftData için bir import eleyin, ardından bu property’yi ContentView struct’a ekleyin;

@Query var students: [Student]

Bu normal bir Student array gibi görünüyor, ancak sadece başına @Query eklemek SwiftData’nın student model container’ından yüklemesi için yeterlidir - otomatik olarak environment’e yerleştirilen main context’i bulur ve container’ı oradan sorgular. Hangi öğrencilerin yükleneceği ya da sonuçların nasıl sıralanacağını belirtmedik, bu yüzden hepsini alacağız.

students ’i normal bir Swift array gibi kullanmaya başlayabiliriz, bu kodu view body’ye yerleştirin;

NavigationStack {
    List(students) { student in
        Text(student.name)
    }
    .navigationTitle("Classroom")
}

İsterseniz kodu çalıştırabilirsiniz, ancak bunun perk bir anlamı yok - liste boş olacak çünkü henüz herhangi bir veri eklemedik, dolayısıyla veritabanımız boş. Bunu düzletmek için listemizin altında, her dokunulduğunda yeni rastgele öğrenci ekleyen bir buton oluşturacağız, ancak daha önce oluşturduğumuz model context’e erişmek için yeni bir property’ye ihtiyacımız var.

Bu property’yi ContentView’e ekleyin;

@Environment(\.modelContext) var modelContext

Bunu yaptıktan sonra, bir sonraki adım rastgele student oluşturan ve bunları model context’e kaydeden bir buton eklemektir.Öğrenciler için firstNames ve lastNames array’leri oluşturarak rastgele isimler atayacağız ve ardından her birinden bir tane seçmek için randomElement() ‘ı kullanacağız.

Bu toolbar’ı List’e ekleyerek başlayın;

.toolbar {
    Button("Add") {
        let firstNames = ["Ginny", "Harry", "Hermione", "Luna", "Ron"]
        let lastNames = ["Granger", "Lovegood", "Potter", "Weasley"]

        let chosenFirstName = firstNames.randomElement()!
        let chosenLastName = lastNames.randomElement()!

        // more code to come
    }
}

Not: Kaçınılmaz olarak randomElement()’e yapılan çağrıları force unwrap yapmamdan şikayet edecek insanlar olacaktır, ancak array’leri değerlere sahip olacak şekilde tam anlamıyla elle oluşturduk - her zaman başarılı olacaktır. Eğer force unwrap’tan umutsuzca nefret ediyorsanız, belki de bunları nil coalescing ve varsayılan değerlerle değiştirin.

Şimdi ilginç kısma gelelim: bir Student nesnesi oluşturacağız. Bunu //more code to come yorumunun yerine ekleyin.

let student = Student(id: UUID(), name: "\(chosenFirstName) \(chosenLastName)")

Son olaraki model context’ten bu student’i eklemesini istememiz gerekir, bu da kaydedileceği anlamına gelir. Bu son satırı button action’a ekleyin.

modelContext.insert(student)

Sonunda, artık uygulamayı çalıştırıp deneyebilirsiniz - rastgele student oluşturmak için Add butonuna birkaç kez tıklayın ve onların listemizde bir yere yerleştiğini görmelisiniz. Daha da iyisi, uygulamayı yeniden başlattığınızda student’lerin hala orada olduğunu göreceksiniz, çünkü SwiftData onları otomatik olarak kaydetmiştir.

Bunun sonuç için çok fazla öğrenme olduğunu düşünebilirsiniz, ancak artık modellerin, model container’ların ve model context’lerin ne olduğunu biliyorsunuz ve verileri nasıl ekleyeceğinizi ve sorgulayacağınızı gördünüz. Bu projenin ilerleyen aşamalarında SwiftData’ya daha fazla bakacağız, ancak şimdilik çok iyi yol katettik.

Bu proje için genel bakışın son bölümüydü, bu yüzden lütfen devam etmeden önce projenizi sıfırlayın. Bu, ContentView.swift, Bookworm.swift’’i sıfırlamak ve Student.swift’i silmek anlamına geliyor.


Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.

Bu yazı, SwiftUI Day 53 adresinde bulunan yazılardan kendim için aldığım notları içermektedir. Orjinal dersi takip etmek için lütfen bağlantıya tıklayın.